//
// Created by qiyu on 12/19/17.
//

#ifndef CINATRA_RESPONSE_HPP
#define CINATRA_RESPONSE_HPP
#include "use_asio.hpp"
#include <string>
#include <vector>
#include <string_view>
#include <chrono>
#include "response_cv.hpp"
#include "itoa.hpp"
#include "utils.hpp"
#include "mime_types.hpp"
#include "session_manager.hpp"
#include "nlohmann_json.hpp"
#include "render.h"
#include "http_cache.hpp"
namespace cinatra {
	class response {
	public:
        response() {
        }

		std::string& response_str(){
            return rep_str_;
        }

        void enable_response_time(bool enable) {
            need_response_time_ = enable;
            if (need_response_time_) {
                char mbstr[50];
                std::time_t tm = std::chrono::system_clock::to_time_t(last_time_);
                std::strftime(mbstr, sizeof(mbstr), "%a, %d %b %Y %T GMT", std::localtime(&tm));
                last_date_str_ = mbstr;
            }
        }

        template<status_type status, req_content_type content_type, size_t N>
        constexpr auto set_status_and_content(const char(&content)[N], content_encoding encoding = content_encoding::none) {
            constexpr auto status_str = to_rep_string(status);
            constexpr auto type_str = to_content_type_str(content_type);
            constexpr auto len_str = num_to_string<N-1>::value;

            rep_str_.append(status_str).append(len_str.data(), len_str.size()).append(type_str).append(rep_server);

            if(need_response_time_)
                append_date_time();
            else
                rep_str_.append("\r\n");

            rep_str_.append(content);
        }

        void append_date_time() {
            using namespace std::chrono_literals;

            auto t = std::chrono::system_clock::now();
            if (t - last_time_ > 1s) {
                char mbstr[50];
                std::time_t tm = std::chrono::system_clock::to_time_t(t);
                std::strftime(mbstr, sizeof(mbstr), "%a, %d %b %Y %T GMT", std::localtime(&tm));
                last_date_str_ = mbstr;
                rep_str_.append("Date: ").append(mbstr).append("\r\n\r\n");
                last_time_ = t;
            }
            else {
                rep_str_.append("Date: ").append(last_date_str_).append("\r\n\r\n");
            }
        }

		void build_response_str() {
			rep_str_.append(to_rep_string(status_));

//			if (keep_alive) {
//				rep_str_.append("Connection: keep-alive\r\n");
//			}
//			else {
//				rep_str_.append("Connection: close\r\n");
//			}

			if (!headers_.empty()) {
				for (auto& header : headers_) {
					rep_str_.append(header.first).append(":").append(header.second).append("\r\n");
				}
				headers_.clear();
			}

			char temp[20] = {};
			itoa_fwd((int)content_.size(), temp);
			rep_str_.append("Content-Length: ").append(temp).append("\r\n");
            if(res_type_!= req_content_type::none){
                rep_str_.append(get_content_type(res_type_));
            }
            rep_str_.append("Server: cinatra\r\n");
			if (session_ != nullptr && session_->is_need_update()) {
				auto cookie_str = session_->get_cookie().to_string();
				rep_str_.append("Set-Cookie: ").append(cookie_str).append("\r\n");
				session_->set_need_update(false);
			}

            if (need_response_time_)
                append_date_time();
            else
                rep_str_.append("\r\n");

			rep_str_.append(std::move(content_));
		}

		std::vector<boost::asio::const_buffer> to_buffers() {
			std::vector<boost::asio::const_buffer> buffers;
			add_header("Host", "cinatra");
			if(session_ != nullptr && session_->is_need_update())
			{
				auto cookie_str = session_->get_cookie().to_string();
				add_header("Set-Cookie",cookie_str.c_str());
                session_->set_need_update(false);
			}
			buffers.reserve(headers_.size() * 4 + 5);
			buffers.emplace_back(to_buffer(status_));
			for (auto const& h : headers_) {
				buffers.emplace_back(boost::asio::buffer(h.first));
				buffers.emplace_back(boost::asio::buffer(name_value_separator));
				buffers.emplace_back(boost::asio::buffer(h.second));
				buffers.emplace_back(boost::asio::buffer(crlf));
			}

			buffers.push_back(boost::asio::buffer(crlf));

			if (body_type_ == content_type::string) {
				buffers.emplace_back(boost::asio::buffer(content_.data(), content_.size()));
			}

			if (http_cache::get().need_cache(raw_url_)) {
				cache_data.clear();
				for (auto& buf : buffers) {
					cache_data.push_back(std::string(boost::asio::buffer_cast<const char*>(buf),boost::asio::buffer_size(buf)));
				}
			}

			return buffers;
		}

		void add_header(std::string&& key, std::string&& value) {
			headers_.emplace_back(std::move(key), std::move(value));
		}

        void clear_headers() {
            headers_.clear();
        }

		void set_status(status_type status) {
			status_ = status;
		}

		status_type get_status() const {
			return status_;
		}

		void set_delay(bool delay) {
			delay_ = delay;
		}

		void set_status_and_content(status_type status) {
			status_ = status;
			set_content(std::string(to_string(status)));
            build_response_str();
		}

		void set_status_and_content(status_type status, std::string&& content, req_content_type res_type = req_content_type::none, content_encoding encoding = content_encoding::none) {
			status_ = status;
            res_type_ = res_type;

#ifdef CINATRA_ENABLE_GZIP
			if (encoding == content_encoding::gzip) {
				std::string encode_str;
				bool r = gzip_codec::compress(std::string_view(content.data(), content.length()), encode_str, true);
				if (!r) {
					set_status_and_content(status_type::internal_server_error, "gzip compress error");
				}
				else {
					add_header("Content-Encoding", "gzip");
					set_content(std::move(encode_str));
				}
			}
			else 
#endif
				set_content(std::move(content));
            build_response_str();
		}

		std::string_view get_content_type(req_content_type type){
            switch (type) {
                case cinatra::req_content_type::html:
                    return rep_html;
                case cinatra::req_content_type::json:
                    return rep_json;
                case cinatra::req_content_type::string:
                    return rep_string;
                case cinatra::req_content_type::multipart:
                    return rep_multipart;
                case cinatra::req_content_type::none:
                default:
                    return "";
            }
        }

		bool need_delay() const {
			return delay_;
		}

		void reset() {
            if(headers_.empty())
			    rep_str_.clear();
            res_type_ = req_content_type::none;
			status_ = status_type::init;
			proc_continue_ = true;
			delay_ = false;
			headers_.clear();
			content_.clear();
            session_ = nullptr;

            if(cache_data.empty())
                cache_data.clear();
		}

		void set_continue(bool con) {
			proc_continue_ = con;
		}

		bool need_continue() const {
			return proc_continue_;
		}

		void set_content(std::string&& content) {
			body_type_ = content_type::string;
			content_ = std::move(content);
		}

		void set_chunked() {
			//"Transfer-Encoding: chunked\r\n"
			add_header("Transfer-Encoding", "chunked");
		}

		std::vector<boost::asio::const_buffer> to_chunked_buffers(const char* chunk_data, size_t length, bool eof) {
			std::vector<boost::asio::const_buffer> buffers;

			if (length > 0) {
				// convert bytes transferred count to a hex string.
				chunk_size_ = to_hex_string(length);

				// Construct chunk based on rfc2616 section 3.6.1
				buffers.push_back(boost::asio::buffer(chunk_size_));
				buffers.push_back(boost::asio::buffer(crlf));
				buffers.push_back(boost::asio::buffer(chunk_data, length));
				buffers.push_back(boost::asio::buffer(crlf));
			}

			//append last-chunk
			if (eof) {
				buffers.push_back(boost::asio::buffer(last_chunk));
				buffers.push_back(boost::asio::buffer(crlf));
			}

			return buffers;
		}

        std::shared_ptr<cinatra::session> start_session(const std::string& name, std::time_t expire = -1,std::string_view domain = "", const std::string &path = "/")
		{
			session_ = session_manager::get().create_session(domain, name, expire, path);
			return session_;
		}

		std::shared_ptr<cinatra::session> start_session()
		{
			if (domain_.empty()) {
				auto host = get_header_value("host");
				if (!host.empty()) {
					size_t pos = host.find(':');
					if (pos != std::string_view::npos) {
						set_domain(host.substr(0, pos));
					}
				}
			}

			session_ = session_manager::get().create_session(domain_, CSESSIONID);
			return session_;
		}

		void set_domain(std::string_view domain) {
			domain_ = domain;
		}

		std::string_view get_domain() {
			return domain_;
		}

		void set_path(std::string_view path) {
			path_ = path;
		}

		std::string_view get_path() {
			return path_;
		}

		void set_url(std::string_view url)
		{
			raw_url_ = url;
		}

		std::string_view get_url(std::string_view url)
		{
			return raw_url_;
		}

		void set_headers(std::pair<phr_header*, size_t> headers) {
			req_headers_ = headers;
		}

        void render_json(const nlohmann::json& json_data)
        {
#ifdef  CINATRA_ENABLE_GZIP
            set_status_and_content(status_type::ok,json_data.dump(),res_content_type::json,content_encoding::gzip);
#else
            set_status_and_content(status_type::ok,json_data.dump(),req_content_type::json,content_encoding::none);
#endif
        }

        void render_string(std::string&& content)
		{
#ifdef  CINATRA_ENABLE_GZIP
			set_status_and_content(status_type::ok,std::move(content),res_content_type::string,content_encoding::gzip);
#else
			set_status_and_content(status_type::ok,std::move(content),req_content_type::string,content_encoding::none);
#endif
		}

		std::vector<std::string> raw_content() {
			return cache_data;
		}

		void redirect(const std::string& url,bool is_forever = false)
		{
			add_header("Location",url.c_str());
			is_forever==false?set_status_and_content(status_type::moved_temporarily):set_status_and_content(status_type::moved_permanently);
		}

		void redirect_post(const std::string& url) {
			add_header("Location", url.c_str());
			set_status_and_content(status_type::temporary_redirect);
		}

		void set_session(std::weak_ptr<cinatra::session> sessionref)
		{
			if(sessionref.lock()){
				session_ = sessionref.lock();
			}
		}

	private:
		std::string_view get_header_value(std::string_view key) const {
			phr_header* headers = req_headers_.first;
			size_t num_headers = req_headers_.second;
			for (size_t i = 0; i < num_headers; i++) {
				if (iequal(headers[i].name, headers[i].name_len, key.data(), key.length()))
					return std::string_view(headers[i].value, headers[i].value_len);
			}

			return {};
		}

		std::string_view raw_url_;
		std::vector<std::pair<std::string, std::string>> headers_;
		std::vector<std::string> cache_data;
		std::string content_;
		content_type body_type_ = content_type::unknown;
		status_type status_ = status_type::init;
		bool proc_continue_ = true;
		std::string chunk_size_;

		bool delay_ = false;

		std::pair<phr_header*, size_t> req_headers_;
		std::string_view domain_;
		std::string_view path_;
		std::shared_ptr<cinatra::session> session_ = nullptr;
		std::string rep_str_;
		std::chrono::system_clock::time_point last_time_ = std::chrono::system_clock::now();
		std::string last_date_str_;
        req_content_type res_type_;
        bool need_response_time_ = false;
	};
}
#endif //CINATRA_RESPONSE_HPP
